Spring cloud 微服务设计原则

AKF 可扩展性原则


Spring cloud 的单一设计原则可以让微服务更加优雅。在这个原则之下所开发的微服务项目通常 只关注单一的功能,有限的业务逻辑 最终完成一个有限的业务逻辑。

就比如 AKF 拆分原则即 扩展立方体(Scalability Cube)或 AKF Scale Cube,需要注意的是 AKF 并不是什么单词缩写,而是一个公司的名称。


我们可以直接概括下这三个轴的主要左右,其中Y轴是功能划分以及基于不同业务之间的划分。而 X轴则是水平扩展,北其名曰水平复制,来砸钱解决问题(加服务器)。最后就是Z轴,Z轴主要左右就是数据区分,来关注服务和数据的优先级进行划分。

Y Axis


Y 轴会会根据基于不同业务之间的整体应用拆分为多个服务,每个服务主要会实现一个单个功能。之后以 Eureka 服务治理为例,通过服务中心来进行治理,最终架构如上所示。

X Axis


X 轴主要是水平扩展,通过复制服务与数据来解决可用性及微服务集群的问题,也就是说将微服务运行多个实例(集群),来通过集群负载均衡的模式来提升服务整体的可用性。

Z Axis

Z轴主要就是区分数据,通常Z轴扩展有两种方案分别为 单元化架构以及数据分区 两种,以大数据时代的今天可能两者都会被使用,

单元化架构

单元化架构的选择主要根据服务的类型以及对象单元是否符合来进行区分,如客户端对服务端节点的选择都将是单元自成一体。

数据分区

数据区分也叫分区(Shard),他其实就是区分数据的,比如说我们可以根据年龄、注册时间来区分数据。通常会包含一下几个数据划分的方式,这也是最为常见的:

  1. 数据类型
  2. 数据范围
  3. 数据热度
  4. 数据分区

这四种最为常见的我们可以在很多大型应用中看到,如 bliblibli 弹幕网,因为咱们也没有看过他们的源代码,但是我们可以随便以肉眼可见的观察下,就会发现他们做了一个非常细致的数据区分。

服务自治

说到 Spring cloud,我们从第三节到现在就一直说他多么多么服务自治,看上去非常高端且难懂。实际上服务自治简单来讲就是 每个微服务都具有独立性质,及运行、开发、测试、构建、部署 且包含数据库都可独立运行的服务。而过多或依赖其他微服务,并与其高度解耦,除此之外我们将此称之为 服务自治。

服务自治也是名副其实的微服务设计原则,因为他可以打来很多的好处,如团队协作、技术栈、后续迭代、持续集成等多种优势。

轻量级通信

轻量级接口通信主要对其要求就是体量较轻,可以跨语言和跨平台之间的通信,以及可以不受技术限制,实现不同通信协议之间的通信,如:REST、RPC、AMQP、STOMP、MQTT等。

接口明确原则

而接口明确原则,则是为了确保之后微服务之间不断的完善,导致因为某个接口的变化而其他服务集群需要进行调整,为了避免此类令人烦恼的事情再次发生,则需要让这些接口在设计开发的时候更加的具有通用性。

容错性设计原则

容错性设计原则也被称之为弹性设计,即依赖服务宕机、网络或硬件出现问题时可以暂停使用服务,切换下一个资源,在后续我们将会以集群的方式解决这个问题。除此之外也有很多成熟可靠的方案进行选择,如 Hystrix 延迟和容错库。

自动化原则

自动化测试

自动化原则主要会分为自动化测试、自动化监控和自动化部署等三种非常主要的自动化方案。其中自动化测试可以减轻测试负担,可以保障微服务的模块在高流量的情况下可访问。

自动化监控

而自动化监控则是监控微服务状态,发现问题以及错误计数等来便于解决和发现问题,也可以根据监控状态来调整负载,来保证服务的稳定性和高可用等。在 Spring cloud 项目中,提供了一个为服务可用性监控 Spring Boot Admin 组件。

自动化部署

自动化部署指的是将代码自动部署到生产环境中,来减少项目发布的时间,目前可以通过 Jenkins + Fastlane 来搭配使用。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Sprng cloud 优缺点

微服务优点

模块化开发

以上图作为参考,我们可以首先得知微服务是以 分布式 作为必要技术。有多个微小服务所组成的一个应用,由于他将一个功能划分为一个很小的服务,可以将一个微服务交给不同的开发人员或团队进行开发。

也就是说不同的开发团队可以同时完成多个属于自己任务范围内的 “微服务”,进行项目开发,因此可以 易于开发和维护,而不是向单一应用一样一个项目几百个文件夹,都涉及不同的功能和服务。

代码量

与单个服务相比,微服务由于功能只针对一个功能所进行提供服务。因此代码量不会像正常的单一应用较多,所以项目的启动速度是比单一应用较快的。(但这也不算是优点,因为启动时还很麻烦,一个一个启动多个微服务)

易于更新迭代

如果只是小型的项目迭代的话,那么微服务由于是多个服务应用组成的一个庞大服务,所以我们可以根据目标 功能对应的微服务 模块直接进行修改后上传。不需要向单体应用一样直接暂停项目并进行修改后发布。

技术栈

由于微服务是以业务单一性为原则的开发模式,因此他的开发也不会是统一一种语言,他可以是多个,且不冲突。

解耦

解耦是在设计模式中较为常见的名词之一,各类书籍中经常出现但就是不给你解释。解耦通俗来说就是两个东西 原来互相影响,现在然他们 分别发展。其核心思想是最小职责,每个地方做一件事情。

通常情况下 松耦合 也可以被称之为 解藕,瞬间让此变得高端大气上档次。因此除了松耦合外,

他还有一个反意为 紧耦合,指的是两个模块之间相互影响且关系紧密,存在着互相关系调用。

具有弹性

由于微服务非常可以简单的实现 服务集群,通过 服务治理组件来实现服务的治理,通常都是两三个集群一起工作。一台蹦了还有另一台,另一台蹦了又换下一台的局面,可以保证服务的正常运行。除此之外还具有水平扩展独立以及扩展性高等优点。

微服务缺点

分布式复杂性

对微服务来说分布式架构是一个非常必要的技术,但因为分布式依赖服务较多,一旦其中某个 依赖服务 以及系统的容错能力都会带来挑战。

分布式事务问题

所谓分布式事务问题可以分为 可靠事件模式(Reliable Event Mode)、补偿模式(Confirm Cancel)、TCC(Try Confirm Cancel) 三种,用于解决分布式的 事务一致性问题

首先我们需要知道为什么要有服务一致性,假设你在 Wechat 中将 100RMB 转账给 B,那么 B 的 Wechat 钱包就会多 100RMB。但如果这个过程中 Wechat 服务崩了,那你将会少 100RMB,而B的钱包数额不变。

可靠事件模式(Reliable Event Mode)

他也被称之为 可靠事件记录协议(RELP,Reliable Event Logging Protocol),是一种用于计算机网络中记录 计算机数据的网络协议

他可以提供消息的可靠传递,常用与不能让数据丢失的环境,如转账。在这里我们需要认清 可靠事件记录协议和可靠事件模式,在通常的情况下,可靠事件模式是可靠事件记录协议的实现

一般情况下事件操作可以被分为三种可能,分别为:

  1. 操作成功,投递事件
  2. 操作失败,投递事件失败
  3. 操作成功,投递失败,抛出异常 | 回滚
本地事件表

因此可靠事件模式主要通过 本地事件表和外部事件表 两者来就进行可靠事件投递。

本地事件表通过将事件和业务数据保存在同一个数据库中,来以一个额外的 事件恢复服务 来恢复事件。

原子性指一个操作不可中断,要么全部成功,要么全部失败的原则即称之为 原子性。

这些都将会由本地事务来保证更新业务的发布事件中的不可中断,要么全部成功,要么全部失败,即原子性。

外部事件表


外部事件表是针对本地事件表出现的问题,单个提出外部事件表的方法。将 事件持久话到外部事件系统服务中,事件系统需要提供一个实时的事件服务以用于接收 为服务发布的事件 。与此同时还需要提供一个恢复服务来确定和恢复事件。

事物提交前,业务系统会通过实时事件服务向系统请求发送事件,事件系统 只记录事件但并不发送

之后业务服务提交后,通过实时事件服务向 =>事件系统 确认发布,事件得到确认后 事件系统 进行发布到消息代理。

当业务系统回滚时,通过实时事件向时间系统去取消事件。假设事件系统的事件恢复服务定期找到未确定发送事件向业务服务查询状态时,根据业务服务返回的状态来决定事件是否需要发布。

补偿模式(Confirm Cancel)


补偿模式主要有 业务异常和技术异常 两个概念,是自身业务所造成的。而技术异常则是非业务逻辑产生的一系列问题,如网络异常等。


因此补偿模式需要使用一个 额外的协调服务来协调微服务保证一致性,这时候协调服务通过按照顺序调用服务,如果某个服务出现异常则直接取消之前所有已经成功调用的服务(所以他也具有原子性)。

也就是说当数据文件为空时提交,那么之前所填写的端口信息以及提交数据都将会被取消,而这所取消的就是一个 补偿

TCC 模式(Try Confirm Cancel)

TCC 模式由 Try、Confirm、Cancel 三个接口,一个完整的 TCC应用由 主业务、若干个业务服务 组成和发起,并最后通过主业务完成整个活动。

TCC 三个接口分别意思为:

  1. try:完成所有业务检查,预留必须存在的业务资源
  2. Confirm:执行业务,不做任何业务的检查和分析,只是用 try 阶段所预留的业务资源,Confirm 操作满足幂等性。

幂等性,就是说一个系统,在同样情况下一次请求和 重复 多次请求对资源造成一致的就是幂等性。

  1. Cancel 释放 try 阶段预留资源(满足幂等性?)

主业务服务调用所有的业务的 try 操作,之后在活动管理器中登记所有业务服务,当所有业务服务的 try 操作都调用成够后或从某个业务服务的 try 操作失败进入 confirm 阶段。

活动管理器根据 try 执行结果来执行 confirm or cencel 操作,如果 try 操作所有都成功则从活动管理器中调用所有 confirm 操作,否则将调用 cancel 来释放 try 预留资源。

分布式同步调用

分布式的同步调用及主要是在不确定环境中无法保证所依赖服务,但可以保证自己可以正常提供服务的情况。

SEDA

阶段式服务器模型(SEDA,Staged event-driven architencure)他是一个软件架构模型,可以将复杂的事件驱动的应用分解为一系列通过队列链接的阶段(Stage)。

在上述中 “分布式的同步调用及主要是在不确定环境中无法保证所依赖服务,但可以保证自己可以正常提供服务的情况。”就可以通过使用 SEDA 进行解决。

拥用一句话概括就是 将请求出来过程划分为多个阶段,不同资源消耗阶段,并使用不同数量的线程进行处理

接口调整

由于微服务数量可能会有很多,因此如果需要调整接口时则需要话费很大的时间。除了调整外还需要写其接口文档,来进行管理。

而接口文档可以从中心点引入很多下游问题,如接口文档的管理和更新等。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud Eureka

本文将会通过 Eureka 来进行服务中心的治理,并实现出 一个 服务中心集群(Eureka Server ……),以及 服务提供者集群(Service Provider ……) 还有一个 服务提供者消费者(Service Consumer) 。之后通过 服务中心 内的 Eureka 来进行服务治理,让 服务消费者(Service Consumer) 调用 服务提供者(Service Provider) 来提供服务。

我们首先使用 Eureka 来实现出一个 服务中心(Eureka Server) 集群,之后通过 Eureaka 所 提供的服务注册(Service Register) 功能来注册 服务提供者(Service Provider),来实现服务提供者集群。

在此后服务消费者(Service Consumer) 使用 Eureka 所提供的 服务注册功能根据服务提供者(Service Provider) 所注册的名称来调用客户端 Feign 来消费 服务提供者所提供的接口

Eureka 是 Netflix 全家桶内项目,他在毕马威在2019年10月所发布的 《颠覆性公司和商业模式》报告中与阿里巴巴分别排名第三,而 Netflix 排名第六位,百度、腾讯分别排名十一和十二位。

虽然 Netlix 排名不如 Alibaba,但他对 Spring cloud 的贡献要早于 Alibaba,因此Netflix 公司所开源的各种项目所组成微服务的核心也不足为奇。

表现层状态转换(Representational State Transfer,REST)由 Roy Thomas Fielding 于 2000年在博士论文中所提出的WWW软件架构风格,目的是放百年在不同软件或程序在网络中相互传递信息,而 Eureka 则是这些项目内基于 REST 服务,主要包含了 服务注册与服务发现功能

构建

Spring Assistant

这从 IDEA 2018 就停止更新了

通过 Spring Assistant 根据自身环境进行选择,当进入选择版本和依赖时,我们需要选择 Spring Cloud Discover -> Eureka Server

Spring Initializr

或者说我们直接通过 IDEA 自带的 Spring Initializr,之后选择 Spring Cloud Discovery -> Eureka Server。当我们构建完成后,肯对会出现一大堆 maven 项目依赖的问题,因此我们需要在 Setting -> Build, Execution, Deployment -> Build Tools -> Maven 下修改 User settings file 所填写的 settings.xml 文件,并写入下述信息,来设置 Alibaba 镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

<mirror>
<id>uk</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://uk.maven.org/maven2/</url>
</mirror>

<mirror>
<id>CN</id>
<name>OSChina Central</name>
<url>http://maven.oschina.net/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

<mirror>
<id>nexus</id>
<name>internal nexus repository</name>
<url>http://repo.maven.apache.org/maven2</url>
<mirrorOf>central</mirrorOf>
</mirror>

</mirrors>
</settings>

服务中心


我们所实现的是 服务中心集群,和普通的单点服务中心不一样的是,我们可以保障在系统崩溃时,可以维持系统的 更高的可用性,所以使用服务中心集群(虽然是服务中心集群,但依然可以通过本文来实现单点服务中心)。

Eureka Server demo

我们可以通过一个标准的 Eureka 模板来进行参考,并通过下述模板来进行修改来实现出我们的 服务中心集群 ,需要注意的是下述code不需要执行(因为你就算执行了实现的也是单点服务中心):

application.properties

src/main/resources/application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 应用名称
spring.application.name=Discovery Server

# Spring 应用端口
server.port=8000

# 节点名称
eureka.instance.hostname=eureka-server.com

# 是否注册到 Eureka Server (默认 true)
eureka.client.register-with-eureka=false

# 是否注册到 Eureka Server 获取注册信息 (默认 true)
eureka.client.fetch-registry=false

# 设置 Eureka Server 交互地址,查询服务和注册服务进行使用(多个以 “,” 分割)
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

DemoApplication

src/main/java/com/example/demo/DemoApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

hosts

为了让效果更加的接近真实环境下多台服务器内应用协同服务,所以我们可以通过添加几个 虚拟地址(DNS) 来创建一个只有本地可以访问的 URL,来进行演示。

在 Windows 环境下,我们需要修改 C:\Windows\System32\drivers\etc\hosts 文件,并添加:

1
127.0.0.1      abc.com

而本文作者使用的是 debian 环境,即 Linux 系统下则需要简单的修改 /etc/hosts 文件即可,并添加:

1
2
3
4
127.0.0.1	eureka-server.com
127.0.0.1 eureka-server-1.com
127.0.0.1 eureka-server-2.com
127.0.0.1 eureka-server-3.com

为了验证其是否生效,可以直接通过 ping xxx.com 进行查看。

Jar Run

之后我们分别在 eureka-server-1、eureka-server-2、eureka-server-3 项目下按照流程点击 maven 侧栏下的 clean、package等命令,以此来代替 mvn 执行 mvn clean | mvn package命令。

当的包项目为 jar 后,分别进入 eureka-server-1、2、3 内该 jar 包所在位置,并指定根据之前所在 hosts 文件下设置的DNS名称运行项目(有多少服务集群搞多少个):

1
java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=eureka-server.com

之后进入到http://eureka-server.com:8000/ 即可访问 Eureka 所提供的信息面板中。

eureka-server-1

如果你理解上述的 Eureka Server demo 的配置并结合下述 eureka-server-1~3 所部署的服务中心集群在 Eureka 控制面板和上图基本相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 应用名称
spring.application.name=server-clustered

# Spring 应用端口
server.port=8001

# 节点名称
eureka.instance.hostname=eureka-server-1.com

# 是否注册到 Eureka Server (默认 true)
eureka.client.register-with-eureka=true

# 是否注册到 Eureka Server 获取注册信息 (默认 true)
eureka.client.fetch-registry=true

# 设置 Eureka Server 交互地址,查询服务和注册服务进行使用(多个以 “,” 分割)
eureka.client.serviceUrl.defaultZone=http://eureka-server-2.com:8002/eureka/,http://eureka-server-3.com:8003/eureka/

eureka-server-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 应用名称
spring.application.name=server-clustered

# Spring 应用端口
server.port=8002

# 节点名称
eureka.instance.hostname=eureka-server-2.com

# 是否注册到 Eureka Server (默认 true)
eureka.client.register-with-eureka=true

# 是否注册到 Eureka Server 获取注册信息 (默认 true)
eureka.client.fetch-registry=true

# 设置 Eureka Server 交互地址,查询服务和注册服务进行使用(多个以 “,” 分割)
eureka.client.serviceUrl.defaultZone=http://eureka-server-1.com:8001/eureka/,http://eureka-server-3.com:8003/eureka/

eureka-server-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 应用名称
spring.application.name=server-clustered

# Spring 应用端口
server.port=8003

# 节点名称
eureka.instance.hostname=eureka-server-3.com

# 是否注册到 Eureka Server (默认 true)
eureka.client.register-with-eureka=true

# 是否注册到 Eureka Server 获取注册信息 (默认 true)
eureka.client.fetch-registry=true

# 设置 Eureka Server 交互地址,查询服务和注册服务进行使用(多个以 “,” 分割)
eureka.client.serviceUrl.defaultZone=http://eureka-server-1.com:8001/eureka/,http://eureka-server-2.com:8002/eureka/

服务提供者


服务提供者(Service provider),我们可以理解为是通过服务中心进行注册,之后为 服务消费者 来提供服务的。我们可以从服务中心,来控制、管理整个微服务的运行、连接状态等。

Eureka Provide demo

DemoApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

我们通过根据之前一样的项目依赖,选择 Eureka Server 即可,进入项目后在启动类中加入 @EnableEurekaClient 注解,来启用其 Eureka 注册和发现功能。

application.properties

1
2
3
4
spring.application.name=server-provide
server.port=8000
provider.name=provider0
eureka.client.serviceUrl.defaultZone=http://eureka-server-3.com:8003/eureka/,http://eureka-server-2.com:8002/eureka/,http://eureka-server-1.com:8001/eureka/

之后我们来设置名称以及控制器需要获取到的信息进行配置,之后通过 eureka.client.serviceUrl.defaultZone来加入服务中心。

HelloController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
@Value("${provider.name}")
private String name;

@Value("${server.port}")
private String port;

@RequestMapping("/home")
public String hello() {
String string = "provider:" + name + "port:" + port;
return string;
}
}

控制器的主要用途是来提供接口让 服务消费者 进行获取的,通过application.properties配置文件内相应内容并根据其@Vlue注解以及 @RestController处理HTTP请求,如果你不觉得麻烦可以用下述两个注解代替:

1
2
@Controller
@ResponseBody

Actuator info


Eureka 在其信息面板中 Instances currently registered with Eureka (目前在Eureka 注册的实例)一览中的 Status(状态)下。为开发者提供了一个便捷查询的入口,即 UP (1) - 192.168.43.65:server-provide:8000 => http://192.168.43.65:8000/actuator/info 这是服务提供者的当前在服务中心的链接状态。

当我们点击跳转时,是可以根据其 pom.xml 以及 application.properties 文件内的配置信息单独写入其 JSON 数据的,虽然 Eureka 默认的为 {}

修改添加后结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"app": {
"name": "eureka-cloud-8000"
},
"company": {
"name": "jiangxue.org.cn"
},
"build": {
"application": "server-provide",
"port": "8000"
}
}

我们需要修改其 application.properties 即 Spring 的全局配置文件:

1
2
3
4
5
6
7
8
9
10
spring.application.name=server-provide
server.port=8000
provider.name=localhost

info.app.name=eureka-cloud-8000
info.company.name=jiangxue.org.cn
info.build.application=${spring.application.name}
info.build.port=${server.port}

eureka.client.serviceUrl.defaultZone=http://eureka-server-3.com:8003/eureka/


之后我们还需要修改 pom.xml 配置文件信息,在 <build></build>元素中的 <plugins> 上方,加入 <finalName>信息:

1
2
3
4
5
6
7
<finalName>${project.artifactId}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>

之后我们可以选择直接在 IDEA 里面运行或通过 jar 包方式运行(虽然是本地环境但还是要整出几十台服务器协同的效果

Jar run


当全部运行完之后,我们将会有三台服务中心集群,以及两台服务提供集群,通过访问我们服务提供集群的控制器即 http://192.168.43.65:8010/hey 都可以正确访问控制器内容时,即可以正常提供服务。

eureka-provide-1

1
2
3
4
5
6
7
8
9
10
spring.application.name=server-provide
server.port=8010
provider.name=localhost

info.app.name=eureka-cloud-${server.port}
info.company.name=jiangxue.org.cn
info.build.application=${spring.application.name}
info.build.port=${server.port}

eureka.client.serviceUrl.defaultZone=http://eureka-server-3.com:8003/eureka/,http://eureka-server-2.com:8002/eureka/,http://eureka-server-1.com:8001/eureka/

eureka-provide-2

1
2
3
4
5
6
7
8
9
10
spring.application.name=server-provide
server.port=8011
provider.name=localhost

info.app.name=eureka-cloud-${server.port}
info.company.name=jiangxue.org.cn
info.build.application=${spring.application.name}
info.build.port=${server.port}

eureka.client.serviceUrl.defaultZone=http://eureka-server-3.com:8003/eureka/,http://eureka-server-2.com:8002/eureka/,http://eureka-server-1.com:8001/eureka/

服务消费者

服务消费者主要通过使用 Feign 来进行实现,Feign 由 Retrofit、JAXRS-2.0 和 WebSocet 所启发,主要用于 Java 到 HTPP 客户端绑定器。因此我们将会通过引入 Feign 依赖来实现服务消费者。

在新建项目时,我们需要注意,需要在 选择依存关系内选择 Spring Web、Eureka Discovery Client。当然之前的服务提供者也可以这样选择,但为了更加方便我们通过选择 Eureka Server 单个依存关系也可以实现。

pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

添加 spring-cloud-starter-openfeign maven 依赖后可能会报错,此时我们只需要通过点击 Reload ALL Maven Projects 按钮来 重新加载所有 Maven 项目

application.properties

1
2
3
4
spring.application.name=service-consumer
server.port=9000
eureka.client.register-with-eureka=false
eureka.client.serviceUrl.defaultZone=http://eureka-server-3.com:8003/eureka/,http://eureka-server-2.com:8002/eureka/,http://eureka-server-1.com:8001/eureka/

DemoApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

通过使用 @EnableDiscoveryClients来启动客户端的服务注册和发现功能,并以 @enableFeignClients 注解来实现远程服务调用。

MyFeignClient interface

1
2
3
4
5
6
7
8
9
10
package com.example.demo;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient(name = "server-provide")
public interface MyFeignClient {
@RequestMapping(value = "/hey")
public String hello();
}

需要注意的是,我们在此处的方法必须与服务提供者(即服务提供者 Controller)的方法一致

之后我们需要定义一个接口,并使用 @FeignClient 注解来通过 name 值在服务中心进行寻找并连接。在这里我们的服务提供者集群在服务中心所注册的名称是 server-provide

ConsumerController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.demo.controller;

import com.example.demo.MyFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConsumerController {
@Autowired
MyFeignClient myFeignClient;

@RequestMapping("/hey")
public String index() {
return myFeignClient.hello();
}
}

Run


最终我们将服务中心集群、服务提供者集群开起来,以及在 IDEA 中启动服务消费者。之后通过访问 http://localhost:9000/hey 刷新两次看看。

如果端口值从 8010~8011 之间不断调换。那么我们通过 Eureka 来进行服务治理所实现的微服务架构完成。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

系统架构发展

单体应用


在互联网的发展初期,用户数量少而造成的一般站点流量也非常的少,但服务器等硬件产品的价格较高。因此一般的开发者会选择将 站点的所有功能 集成在一起,来进行开发,这就是 单体应用。随着单体应用的发展,也出现了 MVC、MVP、MVVM等开发单体应用的程序依然可以满足业务的需求。

优缺点

而单体应用的有优点有

  1. 易于开发、测试、管理、部署
  2. 想对于微服务中可以避免开发

应用缺点

  1. 团队合作困难
  2. 代码的维护的较为无语
  3. 项目的重构很复杂
  4. 对项目的可扩展读有很大的阻碍
  5. 随着项目的资源越来越大,code 越来越多,会造成编译时的速率。

为了解决这个问题,随着访问量的不断增大,数据访问框架 ORM的工作量过载导致数据库崩溃。单体应用可以根据集群来进行应对,由于他的缺点需要对之前的单体应用进行一定的拆分,因此 垂直应用 就出现了人们的视野中。

垂直应用


为了解决上述单体应用随着访问量的不断增大,因此单体应用只能通过集群来进行解决和应对,而在此过程中衍生了垂直应用。垂直应用可以根据业务的需求来解决单体应用的缺点。

垂直应用即 使用分层设计的开发应用,也随着系统复杂度的增加,系统架构的变化和侧重点均不一样。之前介绍的单体应用 当网站流量小的时候,只需要一个应用将所有功能部署一起,来减少部署的节点和成本,而此时的 数据访问框架(ORM) 的工作量多少,成为了服务器是否崩溃的关键。

垂直应用为了解决这个问题,将单一用用拆分成互不干扰的多个应用,来提升效率,此时用于加速前端开发的 MVC 框架成为关键。我们将一个单体应用拆分成了三个单体应用,这样可以 解决高并发,资源分配的问题,并提高项目的扩展、容错性以及资源分配等

水平拆分

垂直应用的拆分可以分为 水平拆分 以及 垂直拆分,其中说平拆分主要是指根据 所属业务也进行划分。但是水平拆分会造成存在 重复造轮子 的问题,且难于维护,而优点是业务拆分后的互相影响较小。

垂直拆分

所谓垂直拆分,即根据按照 系统 来进行拆分,因此 Auth 可以被拆分为 登录和注册。垂直拆分的有点是可以按照分配资源和流量,以及垂直调用之间不会进行影响,但缺点是完全存在 重复造轮子 的问题。

分布式应用应用系统


分布式应用系统是由 若干个独立的计算机集合 ,而成的分布式应用系统。简单概述下就是可以将此堪称两个服务分别运行在两台主机中,他们的相互协作来最终完成一个功能或服务,从理论中来讲这将会被称之为 分布式系统

当然,在上述中我们讲到了垂直应用,如果是相同的程序,将会被称之为 集群,通过不断的 横向扩展 来提升服务能力。

关于横向和纵向扩展

横向和纵向扩展,也有人称之为水平扩展和垂直扩展,我们可以假设下当有一台服务器,每天经历了几千次请求天天崩,因此你需要提升服务器性能。此时 横向扩展就是多增加几台服务器一起进行服务,而 纵向扩展 则是将该服务器换成性能更好的服务器。

服务治理


随着服务数量的不断增加,服务中的资源浪费和调度等问题也会随之而来,此时服务治理(Service-Oriented Architecture (SOA) governance)的作用就起到了一个可以基于访问压力来实时管理集群的容量,从而提高集群的利用率。

为什么要使用 SOA?

  1. SOA 在市场中得到了广泛的使用,可以快速的响应并根据情况作出有效的更改
  2. SOA 解决了 服务间的通信问题 ,通过引入 ESB、技术规范、服务管理规范来解决不同服务之间的通信问题
  3. SOA 解决了 基础服务的梳理 问题,以 SEB 为中心树立和规整了分布式服务
  4. SOA 解决了 业务服务化的问题,将业务为驱动,将业务封装到服务中。
  5. SOA 解决了 服务可复用 的问题,将业务功能设置为通用的业务服务,来实现业务的逻辑复用。

ESB

企业服务总线(ESB,The Enterpries Service Bus)可以将类似总线的基础设施将所有服务连接在一起。并作为服务治理中的通信中心,允许连接多个系统、应用和数据,并无终端地址连接多个系统。

微服务


需要庆幸的是,微服务(Microservices)采用的是服务治理中心,而不是在 SOA 架构中的中心话 ESB,因此服务的调度不需要知道 服务提供者的IP地址、端口号等,住需要知道在服务中心 注册了的服务名即可

微服务指的是将 系统业务功能划分为极小的独立微服务,每个微服务只关注于某个完成的一个小任务。因此系统中的单个微服务可以被独立部署和扩展,在上图中,Provider 1 和 Provider2 在 服务中心(Eureka Server) 注册微服务来让 服务消费者(Service Consumer)进行调用服务(Remote Call)

服务网格


服务网格(Service Mesh)独立与服务之外运行,是服务间通信的基础设施层,可以将它比作是应用程序或微服务之间的 TCP/IP,负责服务之间的网格调用、限流、融断以及监控等。服务网格由 数据平台(Data Plane)和 控制平台(Control Plane) 组成,服务与服务之间通过 边车(Sidecar) 来进行通信,所有边车和网络链接就形成了 服务网格(Service Mesh),其中 Sidecar 主要负责服务发现和容错处理

数据平台与控制平台

数据平台主要用于 处理服务之间的通信,并实现服务的发现,复杂均衡、流量管理、健康检查等。而控制平台主要用于 管理并配置边车(Sidecar)来执行策略和搜集数据

在正常的情况下应用程序的开发人员并不会关系 TCP/IP层,这在服务网格时也依然适用,开发人员也不需要关心服务的融断、断流、限流、监控等,因为这些都将由服务网格处理

特点与区别

一是服务网格的 侧重点 不同,为服务架构更关注服务之间的 生态(如服务治理 SOA) ,而服务网格则更关注服务之间的 通信
二是 侵入性不同,微服务架构实现了架构间分离出来解决问题(解藕),而服务网格则实现了服务框架与服务之间分离出来解决问题。需要值得注意的是服务网格是服务之外独立运行的模块,他提供了微服务框架功能,但不需要在代码和配置中添加相应的依赖库和依赖配置项。

服务网格的特点有:

  1. 对服务没有侵入性
  2. 是一个应用程序之间的中间层
  3. 一个轻量级的网络代理
  4. 应用程序对服务网格无感知
  5. 能够分离出来解决用用程序的重试、监控、追踪和服务发现等问题。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Laravel Email 认证

Laravel 为开发者提供了一种非常简便的方法来实现出 Email 认证,很遗憾的是官方文档所提供的信息并不完整。

发送

MustVerifyEmail

首先,我们直接在 app/User.php 加上 MustVerifyEmail 接口使让其成为 User.php 的接口即可:

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable implements MustVerifyEmail
{
……
}

.env

当配置接口类之后,我们还需要配置 .env 文件,并在此修改其邮箱的服务器以用于发送邮件:

1
2
3
4
5
6
7
8
MAIL_DRIVER=null
MAIL_HOST=null
MAIL_PORT=null
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

保护路由

Laravel 的 Auth\VerificationConteroller 类包含了发送验证链接和看验证 Email 的必要逻辑。这都将通过 verify 选项传递给 Auth::routes方法中,因此我们需要在 web.php 中增加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Auth::routes(['verify'=>true]);
Route::group([],function () {
Route::get('/home', 'HomeController@space')->name('home')->middleware('verified');
});

当我们在 get 路由后加入 middleware('verified') 方法后,将会自动验证当前用户使用邮箱是否经过验证,通过验证的用户将会在 users 数据库表中 email_verified_at字段中添加其验证时间。

验证重定向

如果要设置验证完成后主要通过 app/Http/Controllers/Auth/VerificationController.php 类中的 $redirectTo 属性来进行设置,默认的是跳转至用户的个人空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\VerifiesEmails;

class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be re-sent if the user didn't receive the original email message.
|
*/

use VerifiesEmails;

/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;

……
}

如果在登录后验证邮箱时出错,可通过下述两条 artisan 命令来清理下 Laravel 缓存:

1
2
php artisan config:cache
php artisan cache:clear

文本

邮箱验证

Notifications

在 Laravel 中,我们可以通过使用 php artisan make:notification VerifyEmail 来新建一个 VerifyEmail 邮件通知,正常情况下他默认存储在了 vendor/laravel/framework/src/Illuminate/Auth/Notifications/VerifyEmail.php 下。

此 时在 toMail 方法下,来自定义邮件中的用语:

1
2
3
4
5
return (new MailMessage)
->greeting('Hello!')
->line('One of your invoices has been paid!')
->action('View Invoice', $url)
->line('Thank you for using our application!');

sendEmailVerificationNotification

当你通过使用 artisan 命令创建一个邮件通知后,并不代表已经完成了,还需要在 app/User.php 中添加一个 sendEmailVerificationNotification 即“发送电子邮件验证通知”方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App;

use App\Notifications\VerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable implements MustVerifyEmail
{
……
public function sendEmailVerificationNotification()
{
$this->notify(new VerifyEmail);
}
}

notifications

既然当 Notifications 以及 sendEmailVerificationNotification 都配置完成了,我们还需要来自定义邮件的模板:

1
php artisan vendor:publish --tag=laravel-notifications

此时会将 /vendor/laravel/framework/src/Illuminate/Notifications/resources/views 内容复制到 /resources/views/vendor/notifications 下。

当然你如果需要将所有 Markdown 通知组件导出自己的项目中进行自定义:

1
php artisan vendor:publish --tag=laravel-mail

Laravel 会将/vendor/laravel/framework/src/Illuminate/Mail/resources/views目录内容复制到/resources/views/vendor/mail下。

需要注意的是,该目录会包含除组件之外的 resources/views/vendor/mail/html/themes目录,该目录下存储着默认的主题文件,您可以自行定义并分享。

重置密码

ResetPassword

在进行下一步之后,我们需要来对重置密码的文本模板进行更改,和 邮箱验证 相差无几,需要通过使用 artisan 命令:

1
php artisan make:notification ResetPassword

之后将从 vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php 文件复制到 app/Notifications/ResetPassword.php中,因此我们可以参考 vendor 内的文件结构写入app/Notifications目录下。

sendPasswordResetNotification

ResetPassword 准备完成之后,我们需要通过在app/User.php文件中添加一个sendPasswordResetNotification方法,来使得 ResetPassword 通知被正常使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App;

use App\Notifications\VerifyEmail;
use App\Notifications\ResetPassword;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable implements MustVerifyEmail
{
……
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPassword($token));
}
}

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Laravel Eloquent 序列化

在构建 JSON API 的时候,我们需要通过使用 Eloquent 所提供的一些简便方法来进行实现。需要注意的是我们可以通过设置返回的 header来进行返回 具有人类可读 的输出。

在官方文档中我们可以看到,我们可以序列化为数组或者字符串形式,但实际上,并不是这样 直接返回 return $data 即可返回出 json。因此官方文档所提供的 toJson \ toArray都输出的是Json格式内容。

且返回的数据中文均不是人类具有可读性的 \x000 格式,即 Unicode16 格式,无论你在返回中添加 JSON_UNESCAPED_UNICODE 都无济于事。

序列化

1
2
3
4
5
6
7
public function index()
{
$issue = IssuesModel::all();
return response()->json($issue,200,['Content-Type'=>'application/json;charset=UTF-8',
'Charset'=>'utf-8'],
JSON_UNESCAPED_UNICODE);
}

因此他返回的是一个 具有人类可读 的 JSON 类型文本,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[
{
"id": 1,
"titles": "……",
"datetimes": "2021-05-17 03:27:00",
"names": "……",
"……": "……",
"……": "……",
"……": "……",
"……": "……",
"……": "2021-05-17 03:27:42",
"……": "2021-05-17 03:27:42",
"……": "……",
"……": null,
"……": null,
"……": "……",
"……": null,
"……": null,
"……": "……",
"……": "……",
"……": "……",
"……": "……",
"……": null
}
]

黑名单与白名单

黑名单

在官方的文档中,黑名单被称之为更具直观的 “ 隐藏输出 ”,即从字面意思上理解不输出当前属性的值,我们可以在需要输出的 Eloquent ORM 模型 中保护属性中添加一个 $hidden 属性即可。

1
2
3
4
5
6
7
8
9
10
11
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class TestModel extends Model
{
……
protected $hidden = ['titles','datetimes','names'];
}

因此相应的 json 对象也不会进行输出,如原先的 ['titles','datetimes','names'] 都不会在下述的返回响应中不会输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
"id": 1,
"……": "……",
"……": "……",
"……": "……",
"……": "……",
"……": "2021-05-17 03:27:42",
"……": "2021-05-17 03:27:42",
"……": "……",
"……": null,
"……": null,
"……": "……",
"……": null,
"……": null,
"……": "……",
"……": "……",
"……": "……",
"……": "……",
"……": null
}
]

白名单

白名单在官方文档中也被称之为 “白名单”,因此在本文中我们可以使用一种更为直观的方法来以 “ 需要输出的对象 ” 来更加直观的理解。白名单的输出也非常简单,和上述步骤一样在 Eloquent ORM 模型中添加一个$visible

1
protected $visible = ['titles','datetimes','names'];

因此输出的也是只有相应的 json 对象:

1
2
3
4
5
6
7
8
[
{
"id": 1,
"titles": "……",
"datetimes": "2021-05-17 03:27:00",
"names": "……",
}
]

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Laravel Flysystem

Laravel 通过 Frank de Jonge 内的 Flysystem 扩展包为开发者提供了一个强大的文件系统抽象概念。配置 Flysystem 的配置在config/filesystem.php中,再次我们可以配置所有的文件存储位置以及文件磁盘。

public

public 磁盘被称之为“公共磁盘”,因此他主要适用与公开的访问文件,默认情况下由本地(local)进行驱动,并存储在storage/app/publi目录下。如果需要让此在网络情况下也可以访问,需要创建public/storagestoraage/app/public的符号链接。

这种方式可以将公开文件都保存在一个目录下,以及在使用停机时间部署时可以轻松在不同的部署之间共享这些文件。

此时我们可以通过使用 下述 artisan 命令来创建符号链接:

1
2
$ php artisan storage:link
The [public/storage] directory has been linked.

权限

同样的在 public 中,可见行为目录的 755(拥有读、写、执行权限)和文件的644(拥有读写权限),我们也可修改filesystem配置文件中的权限映射:

1
2
3
4
5
6
7
8
9
10
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'permissions' =>[
'file'=>[
'public'=>0600,
'private'=>0700
]
]
],

在上述配置中,我们将 /public 赋予了0600即只有读写权限,而在 /private 目录下则赋予了 0700的可读、写、执行权限。

文件上传

local


在 Laravel 中,要实现文件的上传需要通过使用 file以及store的搭配,通过 file来告诉 Laravel:”你需要上传谁,及获取谁的表单”。而store则是告诉 Laravel 你需要存储在哪个目录下

1
2
//web.php
Route::post('/upload',"PostController@update");
1
2
3
4
5
6
7
{{--inbar.blade.php--}}
<form action="/upload" method="post" enctype="multipart/form-data">
@csrf
<input type="hidden" name="_token" value="<?php echo csrf_token()?>">
<input type="file" name="img">
<button type="submit">Up File</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
//    Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function update(Request $request)
{
$path = $request->file('img')->store('avatars');
return $path;
}

在上述的 Controller code 中,我们通过了获取名为 img 表单所上传的文件后存储至avatarsstorage/app/avatars目录下。值得注意的是默认情况下 store方法会自动生成一个唯一ID 作为文件名称。我们上传图像的时候,将会在 avatars目录下显示出我们刚刚上传的图像,并经过了store来自动生成一个唯一的ID作为文件名称。

Tencent cos


在上面我们介绍了 Laravel 文件的存储中,说到了文件磁盘,其中主要可以进行本地和远程的文件上传。在 Laravel 我们通常会使用国内较快的 腾讯云 COS,而 Laravel 默认的是 AWS s3,本文主要使用 Tencent cloud cosv5 作为演示,首先需要安装依赖:

安装与配置

1
composer require freyo/flysystem-qcloud-cos-v5

当一切完成之后想 config/app.php 文件内providers中添加以下信息:

1
2
3
4
5
6
7
8

'providers' => [

/*
* Laravel Framework Service Providers...
*/
……
Freyo\Flysystem\QcloudCOSv5\ServiceProvider::class,
env and filesystems

在然后将下述 code 粘贴并修改到 .end文件中:

需要参考腾讯云 COS 管理内和密钥页面进行填写

1
2
3
4
5
6
7
8
9
10
11
12
13
COSV5_APP_ID=
COSV5_SECRET_ID=
COSV5_SECRET_KEY=
COSV5_TOKEN=null
COSV5_TIMEOUT=60
COSV5_CONNECT_TIMEOUT=60
COSV5_BUCKET=
COSV5_REGION=
COSV5_CDN=
COSV5_SCHEME=https
COSV5_READ_FROM_CDN=false
COSV5_CDN_KEY=
COSV5_ENCRYPT=false

当修改完成后将下述 code 复制到 config/filesystems.php 文件内的disks下,无需修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'disks' => [
……
'cosv5' => [
'driver' => 'cosv5',
'region' => env('COSV5_REGION', 'ap-guangzhou'),
'credentials' => [
'appId' => env('COSV5_APP_ID'),
'secretId' => env('COSV5_SECRET_ID'),
'secretKey' => env('COSV5_SECRET_KEY'),
],
'timeout' => env('COSV5_TIMEOUT', 60),
'connect_timeout' => env('COSV5_CONNECT_TIMEOUT', 60),
'bucket' => env('COSV5_BUCKET'),
'cdn' => env('COSV5_CDN'),
'scheme' => env('COSV5_SCHEME', 'https'),
'read_from_cdn' => env('COSV5_READ_FROM_CDN', false),
],

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//    Controller
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function update(Request $request)
{
/*
* 1.获取 名为 img 表单所提交的文件
* 2.选择 cosv5 作为文件磁盘
* 3.将 $file 存储至 "picture_class" 目录下
* 4.生成并返回该文件的 url。
*/

$file = $request->file('img');
$disk = Storage::disk('cosv5');
$file_content = $disk->put("picture_class",$file);
$file_url = $disk->url($file_content);
return ($file_url);
}
文件名称

在 local 一节中我们说道了 store方法会自动生成一个唯一的ID 作为文件的名称,因此在提交的时候唯一性是保证了,但是可读性会大大缩小。因此我们可以通过下述几个方法来对文件名称进行操作:

ID DA
getClientOriginalName 获取原来的文件名
getClientOriginalExtension 获取文件的扩展名
getClientMimeType 文件的类型
1
2
3
4
5
6
7
8
$file = $request->file('img');
$extension = $file->getClientOriginalName();
$filename = date('Y-m-d G_i_s') . ".".$extension;

$disk = Storage::disk('cosv5');
$file_content = $disk->putFileAs('picture_class',$file,$filename);
$file_url = $disk->url($file_content);
return $file_url;

除了上述的 getClient 方法外,我们还需要配合date方法来共同完成,因此我们通过使用 dataY-m-d G_i_s 来进行实现,分别表示 年份-月份-日期 小时_分周_秒数 等,最终输出 2021-05-12 23_46_00

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

redmine

Redmine 是一个基于Web项目的管理和权限管理和跟踪工具,与其他主流的项目管理平台不同的是 Redmine 是一个开源的。它用日历和甘特图辅助项目及进度可视化显示。同时它又支持多项目管理。Redmine 是一个自由开放 源码软件解决方案,它提供集成的项目管理功能,问题跟踪,并为多个版本控制选项的支持。需要注意的是 Redmine 是建立在 Ruby and Rails 的框架之上,因此他支持跨平台和多种数据库,但是缺点是安装的过程会非常折腾。

安装 redmine

下载

最新版的 redmine 可以通过https://www.redmine.org/projects/redmine/wiki/RedmineInstall#Step-9-Test-the-installation 内进行下载,本文使用 4.0.9 (2021-04-26) 作为演示。

需要注意的是下面的所有步骤均在 redmine 文件下进行(除了你 ruby 编译安装)

mysql

首先按照 redmine 官方文档的意思是,我们需要创建一个 redmine 的用户,并创建一个 redmine 库,为了让操作变得简单我们忽略新建用户的这个步骤,需们需要创建一个库:

1
create database redmine

当然你如果非常讲究我们也可以根据官方文档中一样,创建一个 redmine 用户:

1
2
3
CREATE DATABASE redmine CHARACTER SET utf8mb4;
CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'my_password';
GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost';
redmine

之后进入到 redmine-4.0.9/config目录,之后将 database.yml.example 文件下的 production 复制到 database.yml文件下(需要创建一个 database.yml)

1
2
3
4
5
6
7
production:
adapter: mysql2
database: redmine
host: localhost
username: username
password: "password"
encoding: utf8

此时也是一个必然的过程,你还需要修改 production内的username、password等信息,来配置你的数据库帐号密码

ruby

值得注意的是 redmine 所依赖的 ruby&gem and bundle 如果你选择使用 MySQL数据库作为数据存储方式的话还需要有一个 MySQL 的环境。通常在 debian 的系统中已经内置了ruby&gem 的语言环境,如果没有您还需要执行 apt-get install ruby来进行安装,如果您已经存在了ruby&gem则不需要重新安装。

自动安装

本文使用的 ruby版本为2.5.5p157,gem 的版本为 3.2.16,通常 gem 版本可通过使用gem update --system直接进行升级,但需要注意的是

编译安装

本文基于 debian bunsen labs 发行版进行演示,通常除了Arch Linux系的系统基本上都可以进行安装,需要注意的是由于 Arch Linux 滚动更新的缘故,因此他的软件包都非常的新。

现在是 2021年05月11日,此时 debian 系官方提供的 ruby 版本是 2019-03-15 所发布的2.5.5p157版本,而此时的 Arch Linux 可能已经用上了前年,即2020-12-25所发布的 ruby 3.0了,而值得庆幸的是 redmine 也支持 ruby 3.0:

redmine 版本 支持的 ruby 版本 使用的 rails 版本
trunk (>= r20913) Ruby 2.5, 2.6, 2.7, 3.0 Rails 6.1
4.2 Ruby 2.4, 2.5, 2.6, 2.7 Rails 5.2
4.1 Ruby 2.3, 2.4, 2.5, 2.6 Rails 5.2
4.0 Ruby 2.2, 2.3, 2.4, 2.5, 2.6 Rails 5.2
  1. Redmine 4.2不支持Ruby 2.7.0和2.7.1。使用Ruby 2.7.2或更高版本
  2. 24.0.6 之前的 Redmine 支持 Ruby >= 2.2.2。Redmine 4.0.6及更高版本不支持Ruby 2.2。
下载&安装

下载 ruby 2.4.2 版的压缩文件,更多全新版本你可以在 https://cache.ruby-lang.org/pub/ruby/ 进行下载。

1
wget https://cache.ruby-lang.org/pub/ruby/ruby-2.4.2.tar.gz

之后解压并进入文件

1
2
tar -zxvf ruby-2.4.2.tgz
cd ruby-2.4.2

进入后配置并编译源代码

1
2
3
./configure
make
sudo make install

最后通过使用 ruby -v来验证是否安装成功,如果你本机中已经存在了 ruby或其他版本,您需要注意下安装的位置不要与其他版本冲突,否则你无论使用 ruby&gem都会报错:

如果遇到这个问题我会选择将所有 ruby 文件删除,只安装一个 ruby 版本。这个时候你 google 就会有人让你执行 gem install bundler,但是你换个思路想:“我执行 gem update 都报错了,我还能执行 gem install bundler 来安装 bundler 吗?”,很明显这个方法并不适用。如果你有其他的方法欢迎联系我 kl@sif.one

gem

RubyGem(gem),是一个Ruby 的一个包管理器,他提供了一个分发 ruby 程序和库的标准格式,还提供了一个管理程序包的安装工具,类似于 npm、pip。

配置源

RubyGems 一直以来在国内都非常难访问到,在本地你或许可以翻墙,当你要发布上线的时候,你就很难搞了。RubyGems 是由一个非常高端也非常有优越感的一个社区,他们为此提供了一个 Gem 的 CDN,来确保几乎无延迟的同步。

在本文中我们除了使用 RubyChina 所提供的 CDN,以及由 Ruby 官方社区所提供的 GemCDN 源,https://rubygems.org/ 国内是可以访问的,因此我们不需要移出,只需要添加一个 https://gems.ruby-china.com/ 源即可:

1
gem sources -a https://gems.ruby-china.com/

之后通过gem sources来查看源列表存在 rubychina社区所提供的源即可:

1
2
3
4
5
gem source
*** CURRENT SOURCES ***

https://gems.ruby-china.com/
https://rubygems.org/

bundler

安装&配置

bundler 是一个能够跟踪并安装所需特定版本的 gem,以此来为 ruby 项目提供一致的运行环境,安装 bundler 安装的方式可以通过 gem 也可通过外部文件安装:

1
gem install bundler

当然我们也可以制定安装 bundler 版本2.2.17,只需要在安装时指定版本即可:

1
gem install bundler:2.2.17

安装完成后我们需要新的 bundle,来以防下面的操作报错。

1
bundle install

bundler install时你可能会出现两个错误

Gem::Ext::BuildError: ERROR: Failed to build gem native extension error
gem install jaro_winkler -v ‘1.5.2’ –source ‘https://cernerrepos.net/api/gems/rubygems/'

Gem::Installer::ExtensionBuildError: ERROR: Failed to build gem native extension.
如果出现了这种错误我们需要通过 apt-get 安装 install build-essential patch ruby-dev zlib1g-dev liblzma-dev以及nokogiri等依赖:
sudo apt-get install build-essential patch ruby-dev zlib1g-dev liblzma-dev
gem install nokogiri

然后安装 redmine 所需的所有 gem 依赖:

1
bundle install --without development test

接着我们来生成一个随机密钥,rails 使用他来编码存储会话数据的 cookies,从而防止被篡改(重新启动后生成的令牌将会使得现有会话无效)

1
bundle exec rake generate_secret_token
依赖项

本文所使用的 redmine 4.2 版本,因此我们需要使用下面步骤来跳过 rmagick gem 的安装

rmagick 对于 redmine 4.1.0 之前的版本主要用于将甘特图导出至 png 或 pdf 格式文件

1
bundle install --without development test rmagick

rails

在上述配置 redmine 库的时候你可能非常疑问为什么又有新建表,因此在下面的步骤中会给你一个非常好的答案。我们在 redmine 项目下运行下述命令来创建数据结构:

1
RAILS_ENV=production bundle exec rake db:migrate

之后通过下属命令,在数据库中插入默认的配置数据:

1
RAILS_ENV=production bundle exec rake redmine:load_default_data

run


当上述步骤完成之后,我们可以试着通过下述命令运行 redmine:

1
bundle exec rails server webrick -e production

在此时你可能还会遇到一个错误

RubyGems warning/error: Gem::Specification# default_executable = is deprecated with no replacement. It will be removed

这是一个 RubyGem 团队的最新公告声明,这是在 RubyGem 1.8 之后才有的,因此我们需要通过使用下述命令进行更新。
gem pristine –all –no-extensions

当该命令运行成功后,我们可以通过使用localhost:3000进入到 redmine 页面,如果可以看到程序 view 则表示已经部署完成。可以通过使用 admin\admin来登入你的管理员帐号。

redmine-run

读者在运行时可能非常迷惑 redmine 的启动命令太长而记不住,导致每次需要时都需要找到资料粘贴上去。因此为了解决这个问题国内开发者为此开发了 redmine-run 脚本来解决 redmien 启动的问题。

需要注意的是,此项目基于 Linux 4.19.0-16-amd64 #1 SMP Debian 4.19.181-1 (2021-03-19) x86_64 GNU/Linux 环境进行开发,理论上来讲系统只要存在ln以及mv和支持 .sh 脚本基本上就可以使用本项目。

通过使用 redmine run ,可以直接通过redmine来启动 redmine 项目管理平台,避免输入长且毫无规律的 bundle exec rails server webrick -e production,还需要进入到 redmine 目录中运行。

下述操作需要全部使用 root 权限运行

首先我们需要拉取 redmine run:

1
git clone https://gitee.com/analysis-of-river-snow/radmine-run.git

将里面的 install.sh以及redmine.sh拖到 redmine 目录中

redmine 目录即你可以执行bundle exec rails server webrick -e production

让 install.sh 和 redmine.sh 成为可执行文件:

1
2
chmod 777 install.sh
chmod 777 redmine.sh

之后我们先修改 install.sh 文件内容,并将/opt/redmine/redmine-4.0.9/修改为你的 redmine 所在目录即可:

1
2
3
4
5
6
#!/bin/bash

cd /opt/redmine/redmine-4.0.9/
redmine_run=`bundle exec rails server webrick -e production`
echo "[echo] 正在进入目录" $path_use
echo "[echo] 执行 redmine run 命令 => bundle exec rails server webrick -e production" $redmine_run

然后执行./install.sh即可,之后通过使用redmine 来运行项目。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Laravel Factory SQL

Laravel 为开发者提供了 Factory解决数据库的创建以及填充,这也在Laravel v6中被称之为“数据库测试”,但本文中主要根据实际情况来创建一个模型工厂(Model factory)。模型工厂主要用于来新建数据库和相应的模型,最后映射在数据库中,当然也可以进行数据库的随机填充。

如果将 Laravel Factory 与 Java Spring boot 相比的话,单纯通过数据库映射来进行比较,因为 Spring 是根据你在运行时直接判断是否创建字段和数据库的,如果配置正确则在运行时直接给你创建,相比之下 Spring 还是较为方便的。但是如果根据总体功能来将,Laravel Factory 将会更上一层,因为其默认将 fakerr 库作为依赖项,即随着 Laravel 一起进行提供,因此 Factory 还可以对数据库进行随机填充

创建模型

在此之前,希望您已经配置了数据库相关的文件,如数据库名称(dataname)、帐号(username)、密码(password)等配置的正确,以及已经成功构建了一个 Laravel 项目并保证php artisan serv可以正确的运行,在此之后您可以进行新建工厂的步骤(create factory)。

.php artisan make:model Issues -m 通过使用 make:model 来创建一个模型,之后会在 /database/migrations/ 下创建一个名为“2021_05_07_002215_create_issues_table.php”的文件,以及在app/路径下新建一个Issues.php文件,我们可以通过2021_05_07_002215_create_issues_table.php文件中写入数据库表名和字段类型和名称等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateIssuesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('issues', function (Blueprint $table) {
$table->bigIncrements('id');
$table->text('titles');
$table->dateTime('datetimes');
$table->text('names');
$table->text('category');
$table->text('harms');
$table->text('paths');
$table->text('types');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('issues');
}
}

对应的数据库命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create table issues
(
id bigint unsigned auto_increment
primary key,
titles text not null,
datetimes datetime not null,
names text not null,
category text not null,
harms text not null,
paths text not null,
types text not null,
created_at timestamp null,
updated_at timestamp null
)
collate = utf8mb4_unicode_ci;

映射数据库

1
2
3
php artisan migrate
Migrating: 2021_05_07_002215_create_issues_table
Migrated: 2021_05_07_002215_create_issues_table (0.02 seconds)

当我们创建完模型(create model)之后,需要通过使用php artisan migrate来映射到数据库中,此时数据库将会生成并创建一个名为issues的数据表,在此下包含了对应的字段名称和相应类型。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

D3 version4 路径生成器(lines)

D3 的路径生成器在拥有指定坐标的情况下可以生成路径,可以通过使用d3.line()来进行创建一个线生成器。

在 d3 version 3 版本中, 创建路径生成器需要使用 d3.svg.line() 来进行创建,在 v4 中将此变得更加语义化

d3.line()


首先我们使用d3.line()来创建一个默认的线(line)生成器,之后定义一个坐标数组并通过pathData调用lineGenerator传入数组points,之后添加路径(path)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var svg = d3.select('#demo')
.append("svg")
.attr("width",700)
.attr("height",700)

/*
* d3.line()
* 使用默认的配置创建一个 线(line)生成器
* points
* 定义一个坐标数组
*/
var lineGenerator = d3.line()
var points = [
[1, 1],
[100, 100],
[200, 30],
[300, 50],
[400, 40],
[500, 80]
]

/*
* pathData
* -> 通过pathData 调用 lineGenerator来传入数组(points)
* svg
* 选择 svg 元素,添加路径(path),为其绑定数据(pathData)与样式
*/
var pathData = lineGenerator(points)
d3.select('svg')
.append("path")
.attr('d',pathData)
.attr("fill","none")
.attr("stroke","red")

Scale

当然,纯粹的默认路径生成器是满足不了我们的需求的,因为他的路径是根据数据来决定的,因此我们需要来使用一个线性比例尺

线性比例尺(scaleLinear)是一个适用与连续定义数据的比例尺,他很好的保留了比例的差异。每一个 range(y) 中的值都可以被表示为一个函数,其中 domain 对应了 x内的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var svg = d3.select('#demo')
.append("svg")
.attr("width",700)
.attr("height",700)

/*
分别创建一个 xScale 与 yScale 比例尺
*/
var xScale = d3.scaleLinear()
.domain([0,6])
.range([0,600])
var yScale = d3.scaleLinear()
.domain([0,80])
.range([150,0])

/*
定义访问器
*/
var lineGenerator = d3.line()
.x(function (d,i) {
return xScale(i)
})
.y(function (d) {
return yScale(d.value)
})
/*
* d3.line()
* 使用默认的配置创建一个 线(line)生成器
* points
* 定义一个坐标数组
*/
var points = [
{value: 10},
{value: 50},
{value: 10},
{value: 1}
]



/*
* pathData
* -> 通过pathData 调用 lineGenerator来传入数组(points)
* svg
* 选择 svg 元素,添加路径(path),为其绑定数据(pathData)与样式
*/
var pathData = lineGenerator(points)
svg.append("g")
d3.select('g')
.append("path")
.attr('d',pathData)
.attr("fill","none")
.attr("stroke","red")

curve


在 D3 version 4 中新加了一种参数为curve,curve可以在一系列点之间进行差值,最终形成一条连续的线,下述我们使用了d3.curceCardinalcardinal 三次曲线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
var svg = d3.select('#demo')
.append("svg")
.attr("width",700)
.attr("height",700)

svg.append('path')
.attr("fill","none")
.attr("stroke","#999")

/*
* 定义访问器
* curve
* 三次cardinal曲线
*/
var lineGenerator = d3.line()
.x(function (d,i) {
return xScale(i)
})
.y(function (d) {
return yScale(d.value)
})
.curve(d3.curveCardinal)
/*
分别创建一个 xScale 与 yScale 比例尺
*/
var xScale = d3.scaleLinear()
.domain([0,6])
.range([0,600])
var yScale = d3.scaleLinear()
.domain([0,80])
.range([150,0])

/*
* d3.line()
* 使用默认的配置创建一个 线(line)生成器
* points
* 定义一个坐标数组
*/
var points = [
{value: 10},
{value: 50},
{value: 10},
{value: 1}
]


/*
* pathData
* -> 通过pathData 调用 lineGenerator来传入数组(points)
* svg
* 选择 svg 元素,添加路径(path),为其绑定数据(pathData)与样式
*/
var pathData = lineGenerator(points)

d3.select('path')
.attr('d', pathData)

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布